const Metadata = require("./lib/metadata");
const {
	DATASETACTION,
	DATASETFRAME,
	DATASETCOMPUTE,
	DATASETINSPECT,
	DATASETDATA,
	DATASETEDITS,
	DATASETSOURCE,
	DATASETLISTENER,
	DATASETOWNER,
	DATASETRANSACTION,
	DATASETRANSACTIONSTATE,
	DATASETRANSACTIONSTEP,
	DATASETSERVICE,
	DATASETINITALIZE,
	DATASETINITALIZEPS,
	CONTEXT,
	PRIMEKEY,
	DATASETCACHE,
	DATASETCOMPUTEDCACHE,
	DATASETCOMPUTEDCHANGEMAP,
	DATASETCOMPUTEDFILTERS,
	DATASETQUEUE,
	DATASETUNLOADED,
	DATASETUNLOADEDPS
} = require("./lib/const");
const Collector = require("./lib/collector");
const { isPropsChange, protectData, setProp, getDataSetFromViewModuleName, isPropsChangeEqual } = require("./lib/helper");
const Queue = require('./lib/queue');
const NOTSUREACTIONNAME = Symbol('nsan');

class Service {
	static INSPECTPROCESS = {
		CREATE: 'create',
		UPDATE: 'update',
		SOURCE: 'source'
	}

	get context() {
		return this[CONTEXT];
	}

	get request() {
		return this[CONTEXT].request;
	}

	data() {
		return {};
	}

	assign(current, object, ops = {}) {
		let { ignore = [] } = ops, keys = this[PRIMEKEY], t = {};
		Reflect.ownKeys(object || {}).filter(key => keys.includes(key) && !ignore.includes(key) !== -1).forEach(key => {
			if (object[key] !== undefined) {
				t[key] = object[key];
			}
		});
		return Object.assign(current, t);
	}

	onupdate(current, data) {
		this.assign(current, data);
	}

	oncreate(current, data) {
		this.assign(current, data);
	}

	onunload() { }
}

class Inspecter {
	constructor(service, { process, primeKeys, module }) {
		this._task = [];
		this._changed = [];
		this._current = {};
		this._currentChanged = [];
		this._service = service;
		this._process = process;
		this._primeKeys = primeKeys;
		this._module = module;
		this._createProcessCalledProps = new Set();
		this._dist = [];
	}

	get task() {
		return this._task;
	}

	get service() {
		return this._service;
	}

	_setTask(data, changed) {
		this._current = data;
		this._currentChanged = changed;
		changed.forEach(a => !this._changed.includes(a) && this._changed.push(a));
		let inspect = this.service[DATASETINSPECT];
		Reflect.ownKeys(inspect).filter(prop => {
			return this._currentChanged.find(a => a.split(',').shift() === `.${prop}`) !== undefined;
		}).forEach(prop => {
			if (!this._task.includes(prop)) {
				this._task.push(prop);
			}
		});
		if (this._process === 'create') {
			this._createProcessCalledProps.forEach(a => !this._task.includes(a) && this._task.push(a));
		}
		this._task = this._task.filter(a => {
			let t = !this._dist.includes(a);
			if (!t) {
				console.warn(`[ada] inspect prop had excuted [ ${a} ] many times,there may be cyclical effects,${this._module}`);
			}
			return t;
		});
		this._task = this._task.sort((a, b) => this._primeKeys.indexOf(a) - this._primeKeys.indexOf(b));
	}

	_invoke() {
		if (this.task.length > 0) {
			let inspect = this.service[DATASETINSPECT];
			let prop = this.task.shift(), method = inspect[prop];
			let collector = new Collector({
				data: this._current,
				fn: this.service[method]
			});
			return Promise.resolve().then(() => {
				let info = { process: this._process, key: prop };
				if (this._process === 'create') {
					info.isDefaultValue = this._createProcessCalledProps.delete(prop);
				}
				return collector.execute(info, this.service);
			}).then(({ data }) => {
				this._dist.push(prop);
				this._setTask(data, collector.getChangedProps().filter(a => {
					let attr = a.split('.')[1];
					if (attr === prop) {
						console.warn(`[ada] prop[ ${prop} ] effect itself,there may be cyclical effects.parent pass effect to it,it changed itself,it repass to its parent,its parent rerepass to it again... never reach the end.So make sure the pass property is read-only.${this._module}`);
						return false;
					} else {
						if (this._process === 'create') {
							this._createProcessCalledProps.delete(prop);
						}
						return true;
					}
				}));
			}).then(() => {
				return this._invoke();
			});
		} else {
			return Promise.resolve();
		}
	}

	execute(current, changedProps) {
		if (this._process === 'create') {
			Reflect.ownKeys(this.service[DATASETINSPECT]).forEach(prop => {
				!changedProps.includes(`.${prop}`) && this._createProcessCalledProps.add(prop);
			});
		}
		this._setTask(current, changedProps);
		return this._invoke().then(() => {
			return {
				current: this._current,
				changed: this._changed
			}
		});
	}
}

class DataSet {
	constructor() {
		setProp(this, DATASETOWNER, null);
		setProp(this, DATASETDATA, null);
		setProp(this, DATASETSERVICE, null);
		setProp(this, DATASETLISTENER, []);
		setProp(this, DATASETEDITS, []);
		setProp(this, DATASETCOMPUTEDCACHE, null);
		setProp(this, DATASETCOMPUTEDCHANGEMAP, {});
		setProp(this, DATASETCOMPUTEDFILTERS, []);
		setProp(this, DATASETINITALIZE, false);
		setProp(this, DATASETINITALIZEPS, null);
		setProp(this, DATASETQUEUE, new Queue());
		setProp(this, DATASETUNLOADED, false);
		setProp(this, DATASETUNLOADEDPS, null);
	}

	get context() {
		return this[DATASETOWNER].context;
	}

	get isInitalize() {
		return this[DATASETINITALIZE];
	}

	commit(type, data) {
		this.context.logger.group(`COMMIT [${type}] FROM [${this[DATASETOWNER].getModuleName()}]`);
		return this[DATASETQUEUE].add(() => {
			return DataSetHelper.commit(this, type, data).then(() => {
				if (this[DATASETOWNER]) {
					this.context.logger.groupEnd();
				}
			});
		});
	}

	getData() {
		if (!this[DATASETDATA]) {
			this[DATASETDATA] = this[DATASETSERVICE].data();
			this[DATASETSERVICE][PRIMEKEY] = Reflect.ownKeys(this[DATASETDATA] || {}).filter(key => !key.startsWith('_'));
		}
		return this[DATASETDATA];
	}

	getRenderData() {
		let current = this.getData();
		if (!this[DATASETCOMPUTEDCACHE]) {
			let computed = this[DATASETSERVICE][DATASETCOMPUTE], r = {};
			Reflect.ownKeys(computed).forEach(prop => {
				let method = computed[prop];
				let collector = new Collector({
					data: protectData(current),
					fn: this[DATASETSERVICE][method]
				});
				let result = collector.invoke(null, this[DATASETSERVICE], true);
				this[DATASETCOMPUTEDCHANGEMAP][prop] = collector.getUsedProps();
				r[prop] = result;
			});
			this[DATASETCOMPUTEDCACHE] = r;
			this[DATASETCOMPUTEDFILTERS] = Object.keys([DATASETCOMPUTEDCHANGEMAP]).map(a => `.${a}`);
		}
		if (!this[DATASETCACHE]) {
			this[DATASETCACHE] = new Collector({
				data: current,
				fn: (current) => {
					Object.assign(current, this[DATASETCOMPUTEDCACHE] || {});
				}
			}).invoke();
		}
		return this[DATASETCACHE];
	}

	getChangedProps() {
		let originProps = this[DATASETEDITS].filter(a => {
			return this[DATASETCOMPUTEDFILTERS].find(b => a.indexOf(b) === 0) === undefined;
		});
		let computedProps = Reflect.ownKeys(this[DATASETCOMPUTEDCHANGEMAP]).filter(a => {
			return this[DATASETCOMPUTEDCHANGEMAP][a].find(b => originProps.includes(b)) !== undefined;
		}).map(a => `.${a}`);
		return originProps.concat(computedProps);
	}

	_addListener(view, getter, setter, scope) {
		let result = {};
		let index = this[DATASETLISTENER].findIndex(item => item.view === view);
		if (index === -1) {
			let collector = new Collector({
				data: protectData(this.getRenderData()),
				fn: getter
			});
			result = collector.invoke(null, scope);
			setTimeout(() => {
				this[DATASETLISTENER].push({
					useprops: collector.getUsedProps(),
					view,
					getter,
					setter
				});
			});
		} else {
			throw Error("ViewConnector can only invoke connect once with the same DataSet filter");
		}
		return result;
	}

	_removeListener(view) {
		let index = this[DATASETLISTENER].findIndex(item => item.view === view);
		if (index !== -1) {
			this[DATASETLISTENER].splice(index, 1);
		}
	}

	_remove() {
		this[DATASETDATA] = null;
		this[DATASETOWNER] = null;
		this[DATASETSERVICE] = null;
		this[DATASETLISTENER] = [];
		this[DATASETQUEUE].clear();
	}

	_setState(state) {
		this[DATASETDATA] = state;
	}

	_inspectProps(current, changedProps, process) {
		return new Inspecter(this[DATASETSERVICE], process).execute(current, changedProps);
	}

	_updateComputedCache(changedProps) {
		let computed = this[DATASETSERVICE][DATASETCOMPUTE];
		Reflect.ownKeys(this[DATASETCOMPUTEDCHANGEMAP]).forEach(prop => {
			let map = this[DATASETCOMPUTEDCHANGEMAP][prop];
			if (isPropsChange(changedProps, map)) {
				let method = computed[prop];
				let current = this.getData();
				let collector = new Collector({
					data: current,
					fn: this[DATASETSERVICE][method]
				});
				let result = collector.invoke(null, this[DATASETSERVICE], true);
				this[DATASETCOMPUTEDCHANGEMAP][prop] = collector.getUsedProps();
				this[DATASETCOMPUTEDCACHE][prop] = result;
				this[DATASETEDITS].push(`.${prop}`);
			}
		});
	}

	_commitInitalize(data) {
		if (!this[DATASETINITALIZE]) {
			if (!this[DATASETINITALIZEPS]) {
				this[DATASETINITALIZEPS] = this.commit('create', data).then(() => {
					this[DATASETINITALIZE] = true;
					this[DATASETINITALIZEPS] = null;
				});
			}
			return this[DATASETINITALIZEPS];
		} else {
			return Promise.resolve();
		}
	}

	_commitUnload() {
		if (!this[DATASETUNLOADED]) {
			if (!this[DATASETUNLOADEDPS]) {
				this[DATASETUNLOADEDPS] = this.commit('unload').then(() => {
					this[DATASETUNLOADED] = true;
					this[DATASETUNLOADEDPS] = null;
				});
			}
			return this[DATASETUNLOADEDPS];
		}
		return Promise.resolve();
	}
}

class TransactDataSet extends DataSet {
	constructor() {
		super();
		setProp(this, DATASETRANSACTION, []);
		setProp(this, DATASETRANSACTIONSTATE, []);
		setProp(this, DATASETRANSACTIONSTEP, 0);
	}

	commit(type, data) {
		if (this.filterCommit(type)) {
			this[DATASETRANSACTION].unshift({ type, data });
		}
		return super.commit(type, data);
	}

	filterCommit(type) {
		return true;
	}

	getTransactionSize() {
		return Infinity;
	}

	getCommitSize() {
		return this[DATASETRANSACTIONSTATE].length;
	}

	getCurrentStep() {
		return this[DATASETRANSACTIONSTEP];
	}

	getCurrentCommit() {
		return this[DATASETRANSACTION][this.getCurrentStep()];
	}

	getTransactionList() {
		return this[DATASETRANSACTION];
	}

	rollback(step = 1) {
		if (this[DATASETRANSACTIONSTATE].length > 0) {
			this[DATASETRANSACTION].splice(0, step);
			this[DATASETRANSACTIONSTATE].splice(0, step);
			this[DATASETRANSACTIONSTEP] = this[DATASETRANSACTIONSTATE].length;
			this[DATASETDATA] = this[DATASETRANSACTIONSTATE][0];
			this[DATASETCOMPUTEDCACHE] = null;
			this[DATASETCACHE] = null;
			this.context.logger.group(`ROLLBACK FROM [${this[DATASETOWNER].getModuleName()}]`);
			return this[DATASETOWNER]._updateForceFromDataSet().then(() => {
				return DataSetHelper.dispatchForce(this).then(() => {
					this[DATASETEDITS] = [];
					if (this[DATASETOWNER]) {
						this.context.logger.groupEnd();
					}
					return this[DATASETDATA];
				});
			});
		} else {
			return Promise.resolve(this.getRenderData());
		}
	}

	travel(step = 0) {
		step = this[DATASETRANSACTIONSTATE].length - step;
		if (this[DATASETRANSACTIONSTATE].length > 0 && this[DATASETRANSACTIONSTATE][step]) {
			this[DATASETRANSACTIONSTEP] = step;
			this[DATASETDATA] = this[DATASETRANSACTIONSTATE][step];
			this[DATASETCOMPUTEDCACHE] = null;
			this[DATASETCACHE] = null;
			this.context.logger.group(`TRAVEL FROM [${this[DATASETOWNER].getModuleName()}]`);
			return this[DATASETOWNER]._updateForceFromDataSet().then(() => {
				return DataSetHelper.dispatchForce(this).then(() => {
					this[DATASETEDITS] = [];
					if (this[DATASETOWNER]) {
						this.context.logger.groupEnd();
					}
					return this[DATASETDATA];
				});
			});
		} else {
			return Promise.resolve(this.getRenderData());
		}
	}

	resetHistory() {
		setProp(this, DATASETRANSACTION, []);
		setProp(this, DATASETRANSACTIONSTATE, []);
		setProp(this, DATASETRANSACTIONSTEP, 0);
	}

	_setState(state, type) {
		if (!type || this.filterCommit(type)) {
			if (this[DATASETRANSACTIONSTATE].length > this.getTransactionSize()) {
				this[DATASETRANSACTIONSTATE].shift();
			}
			this[DATASETRANSACTIONSTATE].unshift(state);
			this[DATASETRANSACTIONSTEP] = this[DATASETRANSACTIONSTATE].length - 1;
		}
		this[DATASETDATA] = state;
	}
}

class DataSetHelper {
	static getDataSet({ type = DataSet, service = Service }, owner) {
		let _dataset = new type();
		_dataset[DATASETOWNER] = owner;
		if (service.prototype instanceof Service) {
			_dataset[DATASETSERVICE] = new service();
		} else {
			throw Error("[ada] must be a Service class");
		}
		let actions = Object.assign({ update: "onupdate", create: 'oncreate', unload: 'onunload' }, Metadata.getMetadataExtends("action", _dataset[DATASETSERVICE].constructor.prototype) || {});
		let _frame = Metadata.getMetadataExtends("frames", _dataset[DATASETSERVICE].constructor.prototype) || {}, frame = {};
		Reflect.ownKeys(_frame).forEach(methodName => {
			let has = false;
			frame[methodName] = (_frame[methodName] || []).map(a => {
				if (a === '@' || a === 'self') {
					has = true;
					return { action: NOTSUREACTIONNAME, method: methodName };
				} else {
					return { action: a, method: actions[a] };
				}
			});
			if (!has) {
				frame[methodName].unshift({ action: NOTSUREACTIONNAME, method: methodName });
			}
		});
		_dataset[DATASETSERVICE][DATASETFRAME] = frame;
		_dataset[DATASETSERVICE][DATASETACTION] = actions;
		_dataset[DATASETSERVICE][DATASETCOMPUTE] = Metadata.getMetadataExtends("compute", _dataset[DATASETSERVICE].constructor.prototype) || {};
		_dataset[DATASETSERVICE][DATASETINSPECT] = Metadata.getMetadataExtends("inspect", _dataset[DATASETSERVICE].constructor.prototype) || {};
		_dataset[DATASETSERVICE][CONTEXT] = owner.context;
		let source = Metadata.getMetadataExtends("source", _dataset[DATASETSERVICE].constructor.prototype) || {};
		_dataset[DATASETSERVICE][DATASETSOURCE] = source;
		Reflect.ownKeys(source).forEach(methodName => {
			let { target, name } = source[methodName];
			let setter = (data, value) => {
				data[name] = value;
			};
			let targetDataset = getDataSetFromViewModuleName(owner, target);
			let params = null;
			if (targetDataset) {
				let getter = _dataset[DATASETSERVICE][methodName];
				params = targetDataset._addListener(owner, getter, setter, _dataset[DATASETSERVICE]);
			} else {
				params = _dataset[DATASETSERVICE][methodName](null);
			}
			_dataset._setState(new Collector({
				data: _dataset.getData(),
				fn: setter
			}).execute(params).data || {});
		});
		return _dataset;
	}

	static commit(dataset, type, data) {
		if (dataset[DATASETSERVICE]) {
			let method = dataset[DATASETSERVICE][DATASETACTION][type];
			if (method && dataset[DATASETSERVICE][method]) {
				let frames = dataset[DATASETSERVICE][DATASETFRAME][method] || [], next = true;
				if (frames.length === 0) {
					frames = [{ action: type, method }];
				}
				frames.forEach(a => a.action === NOTSUREACTIONNAME && (a.action = type));
				return Promise.resolve().then(() => {
					if (dataset[DATASETOWNER]) {
						return dataset[DATASETOWNER].onframesstart({ action: type, frames: frames.map(a => a.action) });
					}
				}).then(() => {
					let ps = Promise.resolve();
					frames.forEach(({ action, method: name }) => {
						ps = ps.then(() => {
							if (next !== false) {
								if (dataset[DATASETSERVICE][name]) {
									let collector = new Collector({
										data: dataset.getData(),
										fn: dataset[DATASETSERVICE][name]
									});
									return Promise.resolve().then(() => {
										return collector.execute(protectData(data), dataset[DATASETSERVICE]);
									}).then(({ data: info, result }) => {
										if (dataset[DATASETOWNER]) {
											let s = Promise.resolve({
												current: info,
												changed: collector.getChangedProps()
											});
											if (type === 'create') {
												s = s.then(({ current, changed }) => {
													let source = dataset[DATASETSERVICE][DATASETSOURCE];
													let primeKeys = dataset[DATASETSERVICE][PRIMEKEY];
													Reflect.ownKeys(source).map(methodName => {
														let prop = `.${source[methodName].name}`;
														if (!changed.includes(prop)) {
															changed.push(prop);
														}
														if (primeKeys.includes(source[methodName].name)) {
															console.warn(`[ada] @source prop[ ${source[methodName].name} ] and key prop can't coexist,module is ${dataset[DATASETOWNER].getModuleName()}`);
														}
													});
													return dataset._inspectProps(current, changed, {
														process: 'create',
														primeKeys,
														module: dataset[DATASETOWNER].getModuleName()
													});
												});
											} else if (type === 'update') {
												s = s.then(({ current, changed }) => {
													return dataset._inspectProps(current, changed, {
														process: 'update',
														primeKeys: dataset[DATASETSERVICE][PRIMEKEY],
														module: dataset[DATASETOWNER].getModuleName()
													});
												});
											}
											return s.then(({ current, changed }) => {
												dataset[DATASETEDITS] = changed;
												dataset[DATASETCACHE] = null;
												dataset._setState(current || {}, type);
												dataset._updateComputedCache(dataset[DATASETEDITS]);
											}).then(() => {
												return dataset[DATASETOWNER]._updateFromDataSet();
											}).then(() => {
												return DataSetHelper.dispatch(dataset);
											}).then(() => {
												dataset[DATASETEDITS] = [];
												next = result;
											});
										}
									}).then(() => {
										if (dataset[DATASETOWNER]) {
											return dataset[DATASETOWNER].onframeout({ action: type, frame: action, invoked: true });
										}
									});
								} else {
									console.warn(`can not found action method name is ${name}`);
								}
							} else {
								if (dataset[DATASETOWNER]) {
									return dataset[DATASETOWNER].onframeout({ action: type, frame: action, invoked: false });
								}
							}
						});
					});
					return ps;
				}).then(() => {
					if (dataset[DATASETOWNER]) {
						return dataset[DATASETOWNER].onframesend();
					}
				}).catch(info => {
					console.error(info);
				}).then(() => {
					return Promise.resolve().then(() => dataset[DATASETOWNER] && dataset[DATASETOWNER].oncommited(type));
				});
			}
			return Promise.reject(`[ada] service action is undefined name is ${type}`);
		} else {
			return Promise.resolve();
		}
	}

	static dispatchDatasetListeners(dataset, list) {
		return list.reduce((a, b) => {
			return a.then(() => {
				if (!b.view.isRemoved()) {
					let collector = new Collector({
						data: protectData(dataset.getRenderData()),
						fn: b.getter
					});
					let _dataset = b.view.getDataSet();
					let result = collector.invoke(null, _dataset[DATASETSERVICE]);
					b.useprops = collector.getUsedProps();
					let _collector = new Collector({
						data: _dataset.getData(),
						fn: b.setter
					});
					let { data: info } = _collector.execute(result);
					return _dataset._inspectProps(info, _collector.getChangedProps(), {
						process: 'source',
						primeKeys: _dataset[DATASETSERVICE][PRIMEKEY],
						module: _dataset[DATASETOWNER].getModuleName()
					}).then(({ current, changed }) => {
						_dataset[DATASETEDITS] = changed;
						_dataset[DATASETCACHE] = null;
						_dataset._setState(current || {});
						_dataset._updateComputedCache(_dataset[DATASETEDITS]);
					}).then(() => {
						return _dataset[DATASETOWNER]._updateFromDataSet();
					}).then(() => {
						return DataSetHelper.dispatch(_dataset);
					}).then(() => {
						_dataset[DATASETEDITS] = [];
					});
				} else {
					return Promise.resolve();
				}
			});
		}, Promise.resolve());
	}

	static dispatch(dataset) {
		return DataSetHelper.dispatchDatasetListeners(dataset, dataset[DATASETLISTENER].filter(info => {
			return isPropsChange(dataset.getChangedProps(), info.useprops, dataset[DATASETOWNER]);
		}));
	}

	static dispatchForce(dataset) {
		return DataSetHelper.dispatchDatasetListeners(dataset, dataset[DATASETLISTENER]);
	}
}

module.exports = {
	Service,
	DataSet,
	TransactDataSet,
	DataSetHelper
};